#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <cstdlib>
#include <cmath>
#include "realvariablebag.hpp"
#include "stringvariablebag.hpp"
#include <stack>
#include <cctype>
#include <algorithm>
#include "BASIC_like_std_string_functions.hpp"

using namespace std;

enum variableType {real, str};
struct variable
{
  enum variableType type;
  double realValue;
  std::string stringValue;
};

struct line
{
  unsigned lineNumber;
  string instruction;
};
vector<line> lines;  //This contains the program

void loadProgram(char *fileName), executeInstructions(), errorAndTerminate(const char *s);
void doPRINT(string s, realvariablebag &rvb, stringvariablebag &svb), doLET(string s, realvariablebag &rvb, stringvariablebag &svb);
void readAllDATA(vector<variable> &dataList), doINPUT(string s, realvariablebag &rvb, stringvariablebag & svb);
void doREAD(string s, realvariablebag &rvb, stringvariablebag &svb, vector<variable> &dataList, unsigned &dataPosition);
unsigned doGOTO(string s), doGOSUB(string s, stack<unsigned> &gosubStack), doRETURN(stack<unsigned> &gosubStack);
variable eval(string expression, realvariablebag &rvb, stringvariablebag &svb);
variable level2(string &expression, realvariablebag &rvb, stringvariablebag &svb);
void level3(string &expression, realvariablebag &rvb, stringvariablebag &svb, variable &result);
void level4(string &expression, realvariablebag &rvb, stringvariablebag &svb, variable &result);
variable arith(char op, variable l, variable r);

int main(int argc, char *argv[])
{
  if(argc != 2)
    cout << argc;
    errorAndTerminate("Bad command line arguments");
  loadProgram(argv[1]);
  executeInstructions();
  return EXIT_SUCCESS;
}

void loadProgram(char *fileName)
{
  ifstream myfile(fileName);
  string s;
  unsigned number;
  line lineOfCode;
  if(myfile.is_open())
  {
    while(!myfile.eof())
    {
      getline(myfile, s);
      s = TRIM(s);
      if(s.size() == 0)  //blank
        continue;
      number = 0;
      while(!s.empty() && isdigit(s[0]))
      {
        number = number * 10 + (s[0] - '0');
        s.erase(0, 1);
      }
      if(number == 0)
        errorAndTerminate("Bad line number");
      lineOfCode.instruction = LTRIM(s);
      lineOfCode.lineNumber = number;
      lines.push_back(lineOfCode);
    }
  }
  else
    errorAndTerminate("Unable to open file");
}

void executeInstructions()
{
  unsigned currentLine = 0;
  vector<variable> dataList;
  unsigned dataPosition = 0;
  realvariablebag rvb;
  stringvariablebag svb;
  stack<unsigned> gosubStack;
  readAllDATA(dataList);
  while(currentLine < lines.size())
  {
    start:
    if(UCASE(lines[currentLine].instruction.substr(0, 3)) == "REM" || UCASE(lines[currentLine].instruction.substr(0, 4)) == "DATA")
    {
      ;
    }
    else if(UCASE(lines[currentLine].instruction) == "CLS")
    {
      system("CLS");  //note: Windows only
    }
    else if(UCASE(lines[currentLine].instruction) == "END")
    {
      exit(EXIT_SUCCESS);
    }
    else if(UCASE(lines[currentLine].instruction) == "BEEP")
    {
      cout << '\a';
    }
    else if(UCASE(lines[currentLine].instruction.substr(0, 4)) == "GOTO")
    {
      string s = lines[currentLine].instruction;
      s.erase(0, 4);
      currentLine = doGOTO(s);
      goto start;
    }
    else if(UCASE(lines[currentLine].instruction.substr(0, 3)) == "LET")
    {
      string s = lines[currentLine].instruction;
      s.erase(0, 3);
      doLET(s, rvb, svb);
    }
    else if(UCASE(lines[currentLine].instruction.substr(0, 5)) == "PRINT")
    {
      string s = lines[currentLine].instruction;
      s.erase(0, 5);
      doPRINT(s, rvb, svb);
    }
    else if(UCASE(lines[currentLine].instruction.substr(0, 5)) == "INPUT")
    {
      string s = lines[currentLine].instruction;
      s.erase(0, 5);
      doINPUT(s, rvb, svb);
    }
    else if(UCASE(lines[currentLine].instruction.substr(0, 4)) == "READ")
    {
      string s = lines[currentLine].instruction;
      s.erase(0, 4);
      doREAD(s, rvb, svb, dataList, dataPosition);
    }
    else if(UCASE(lines[currentLine].instruction.substr(0, 5)) == "GOSUB")
    {
      string s = lines[currentLine].instruction;
      s.erase(0, 5);
      currentLine = doGOSUB(s, gosubStack);
      goto start;
    }
    else if(UCASE(lines[currentLine].instruction) == "RETURN")
    {
      currentLine = doRETURN(gosubStack);
    }
    else
      errorAndTerminate("Command not recognised");
    ++currentLine;
  }
}

unsigned doGOTO(string s)
{
  unsigned number;
  s = LTRIM(s);
  if(s.empty() || !isdigit(s[0]))
    errorAndTerminate("Number expected");
  number = unsigned(atoi(s.c_str()));
  for(unsigned i = 0; i<lines.size(); i++)
  {
    if(lines[i].lineNumber == number)
      return i;
  }
  errorAndTerminate("You tried to GOTO a line that does not exist");
  return 0; //suppress warning never actually executes
}

void doPRINT(string s, realvariablebag &rvb, stringvariablebag &svb)
{
  s = TRIM(s);
  if(s.empty())
  {
    cout << endl;
    return;
  }
  char lastCh = s[(s.size()-1)];
  string expr;
  variable output;
  while(!s.empty())
  {
    while(!s.empty() || (!s.empty() && s[0] != ';'))
    {
      if(s[0] == '\"')
      {
        expr = '\"';
        s.erase(0, 1);
        while(!s.empty() && s[0] != '\"')
        {
          expr.push_back(s[0]);
          s.erase(0, 1);
        }
        if(s.empty())
          errorAndTerminate("\'\"\' expected");
      }
      expr.push_back(s[0]);
      s.erase(0, 1);
    }
    output = eval(expr, rvb, svb);
    if(output.type == real)
      cout << output.realValue;
    if(output.type == str)
      cout << output.stringValue;
    expr.clear();
  }
  if(lastCh != ';')
    cout << endl;
}

void doLET(string s, realvariablebag &rvb, stringvariablebag &svb)
{
  string name;
  s = TRIM(s);
  variable value;
  if(s.empty())
    errorAndTerminate("Syntax error");
  if(isalpha(s[0]))
  {
    name = s[0];
    s.erase(0, 1);
    while(!s.empty() && (isalnum(s[0]) || s[0] == '_'))
    {
      name.push_back(s[0]);
      s.erase(0, 1);
    }
    if(!s.empty() && s[0] == '$')
    {
      name.push_back('$');
      s.erase(0, 1);
    }
    s = LTRIM(s);
    if(!s.empty() && s[0] == '=')
    {
      s.erase(0, 1);
      value = eval(s, rvb, svb);
      if(value.type == real && name[(name.size() - 1)] != '$')
        rvb.set(name, value.realValue);
      else if(value.type == str && name[(name.size() - 1)] == '$')
        svb.set(name, value.stringValue);
      else
        errorAndTerminate("Type mismatch");
    }
    else
      errorAndTerminate("\'=\' expected");
  }
  else
    errorAndTerminate("Invalid name");
}

void readAllDATA(vector<variable> &dataList)
{
  variable constant;
  string valueAsString, s;
  for(unsigned i = 0; i < lines.size(); i++)
  {
    if(UCASE(lines[i].instruction.substr(0, 4)) == "DATA")
    {
      s = lines[i].instruction;
      s.erase(0, 4);
      s = TRIM(s);
      while(true)
      {
        if(!s.empty() && s[0] == '\"')
        {
          s.erase(0, 1);
          while(!s.empty() && s[0] != '\"')
          {
            valueAsString.push_back(s[0]);
            s.erase(0, 1);
          }
          if(s.empty())
            errorAndTerminate("\'\"\' expected");
          s.erase(0, 1);
          constant.type = str;
          constant.stringValue = valueAsString;
          dataList.push_back(constant);
        }
        else
        {
          while(!s.empty() || (!s.empty() && s[0] != ','))
          {
            valueAsString.push_back(s[0]);
            s.erase(0, 1);
          }
          constant.type = real;
          constant.realValue= atof(valueAsString.c_str());
          dataList.push_back(constant);
        }
        valueAsString.clear();
        s = LTRIM(s);
        if(s.empty())
          return;
        else if(s[0] != ',')
          errorAndTerminate("\',\' expected");
        s.erase(0, 1);
      }
    }
  }
}

void doINPUT(string s, realvariablebag &rvb, stringvariablebag & svb)
{
  string name, expr, inputString;
  variable printPart;
  s = TRIM(s);
  if(s.empty())
    errorAndTerminate("Syntax error");
  if(isalpha(s[0]))
  {
    name = s[0];
    s.erase(0, 1);
    while(!s.empty() && (isalpha(s[0]) || s[0] == '_'))
    {
      name.push_back(s[0]);
      s.erase(0, 1);
    }
    if(!s.empty() && s[0] == '$')
    {
      name.push_back(s[0]);
      s.erase(0, 1);
    }
    if(s.empty())
    {
      cout << "? " << flush;
      getline(cin, inputString);
      if(name[(name.size() - 1)] != '$')
        rvb.set(name, atof(inputString.c_str()));
      else
        svb.set(name, inputString);
      return;
    }
  }
  expr = name;
  while(!s.empty() || (!s.empty() && s[0] != ';'))
  {
    if(s[0] == '\"')
    {
      expr.push_back('\"');
      s.erase(0, 1);
      while(!s.empty() && s[0] != '\"')
      {
        expr.push_back(s[0]);
        s.erase(0, 1);
      }
      if(s.empty())
        errorAndTerminate("\'\"\' expected");
      expr.push_back('\"');
      s.erase(0, 1);
    }
    expr.push_back(s[0]);
    s.erase(0, 1);
  }
  if(s.empty())
    errorAndTerminate("\';\' expected");
  s.erase(0, 1);
  if(s.empty())
    errorAndTerminate("Variable name expected");
  name = s;
  printPart = eval(expr, rvb, svb);
  if(printPart.type == real)
    cout << printPart.realValue << flush;
  else
    cout << printPart.stringValue << flush;
  getline(cin, inputString);
  if(name[(name.size() - 1)] != '$')
    rvb.set(name, atof(inputString.c_str()));
  else
    svb.set(name, inputString);
}

void doREAD(string s, realvariablebag &rvb, stringvariablebag &svb, vector<variable> &dataList, unsigned &dataPosition)
{
  string name;
  s = TRIM(s);
  if(s.empty())
    errorAndTerminate("Syntax error");
  while(!s.empty())
  {
    while(!s.empty() && s[0] != ',')
    {
      name.push_back(s[0]);
      s.erase(0, 1);
    }
    name = TRIM(name);
    if(name.empty())
      errorAndTerminate("Variable name expected");
    if(dataList.size() > dataPosition)
    {
      if(name[(name.size() - 1)] == '$' && dataList[dataPosition].type == str)
        svb.set(name, dataList[dataPosition].stringValue);
      else if(name[(name.size() - 1)] != '$' && dataList[dataPosition].type == real)
        rvb.set(name, dataList[dataPosition].realValue);
      else
        errorAndTerminate("Type mismatch");
      ++dataPosition;
      name.clear();
    }
    else
      errorAndTerminate("Out of data");
    s = TRIM(s);
  }
}

void errorAndTerminate(const char *s)
{
  cout << "Error: " << s << endl;
  system("pause");
  exit(EXIT_FAILURE);
}

unsigned doGOSUB(string s, stack<unsigned> &gosubStack)
{
  unsigned number;
  s = TRIM(s);
  if(s.empty() || !isdigit(s[0]))
    errorAndTerminate("Number expected");
  number = unsigned(atoi(s.c_str()));
  for(unsigned i = 0; i < lines.size(); i++)
  {
    if(lines[i].lineNumber == number)
      return i;
  }
  errorAndTerminate("You tried to GOSUB a line that does not exist");
  return 0; //suppress warning will never execute
}

unsigned doRETURN(stack<unsigned> &gosubStack)
{
  if(gosubStack.empty())
    errorAndTerminate("RETURN without GOSUB");
  unsigned number = gosubStack.top();
  gosubStack.pop();
  return number;
}

variable eval(string expression, realvariablebag &rvb, stringvariablebag &svb)
{
  expression = TRIM(expression);
  if(expression.empty())
    errorAndTerminate("No expression present");
  return level2(expression, rvb, svb);
}

variable level2(string &expression, realvariablebag &rvb, stringvariablebag &svb)
{
  variable result, hold;
  char op;
  level3(expression, rvb, svb, result);
  expression = LTRIM(expression);
  while(!expression.empty() && (expression[0] == '+' || expression[0] == '-'))
  {
    op = expression[0];
    expression.erase(0, 1);
    level3(expression, rvb, svb, hold);
    result = arith(op, result, hold);
    expression = LTRIM(expression);
  }
  return result;
}

void level3(string &expression, realvariablebag &rvb, stringvariablebag &svb, variable &result)
{
  variable hold;
  char op;
  level4(expression, rvb, svb, result);
  expression = LTRIM(expression);
  while(!expression.empty() && (expression[0] == '*' || expression[0] == '/'))
  {
    op = expression[0];
    expression.erase(0, 1);
    level4(expression, rvb, svb, hold);
    result = arith(op, result, hold);
    expression = LTRIM(expression);
  }
}

void level4(string &expression, realvariablebag &rvb, stringvariablebag &svb, variable &result)
{
  expression = LTRIM(expression);
  if(expression.empty())
    errorAndTerminate("Error parsing");
  if(isdigit(expression[0]))  //number
  {
    string s;
    do
    {
      s.push_back(expression[0]);
      expression.erase(0, 1);
    }while(!expression.empty() && isdigit(expression[0]));
    if(!expression.empty() && expression[0] == '.')
    {
      s.push_back('.');
      expression.erase(0, 1);
      while(!expression.empty() && isdigit(expression[0]))
      {
        s.push_back(expression[0]);
        expression.erase(0, 1);
      }
    }
    result.type = real;
    result.realValue = atof(s.c_str());
  }
  else if(expression[0] =='\"') //string literal
  {
    string s;
    expression.erase(0, 1);
    while(!expression.empty() && expression[0] != '\"')
    {
      s.push_back(expression[0]);
      expression.erase(0, 1);
    }
    if(expression.empty())  //no closing quote
      errorAndTerminate("Double quote expected");
    expression.erase(0, 1);
    result.type = str;
    result.stringValue = s;
  }
  else if(expression[0] == '(')
  {
    int parendepth = 1;
    string s;
    expression.erase(0, 1);
    while(true)
    {
      if(expression.empty())
        errorAndTerminate("\')\' expected");
      if(expression[0] == '\"')
      {
        s.push_back('\"');
        expression.erase(0, 1);
        while(!expression.empty() && expression[0] != '\"')
        {
          s.push_back(expression[0]);
          expression.erase(0, 1);
        }
        if(expression.empty())
          errorAndTerminate("\" expected");
        s.push_back('\"');
        expression.erase(0, 1);
      }
      else if(expression[0] == '(')
      {
        s.push_back('(');
        expression.erase(0, 1);
        ++parendepth;
      }
      else if(expression[0] == ')')
      {
        --parendepth;
        if(parendepth == 0)
        {
          expression.erase(0, 1);
          break;
        }
        s.push_back(')');
        expression.erase(0, 1);
      }
      else
      {
        s.push_back(expression[0]);
        expression.erase(0, 1);
      }
    }
    result = eval(s, rvb, svb);
  }
  else if(isalpha(expression[0])) //function or variable
  {
    string name;
    name = expression[0];
    expression.erase(0, 1);
    while(!expression.empty() && (isalnum(expression[0]) || expression[0] == '_'))
    {
      name.push_back(expression[0]);
      expression.erase(0, 1);
    }
    if(!expression.empty() && expression[0] == '$')
    {
      name.push_back('$');
      expression.erase(0, 1);
    }
    expression = LTRIM(expression);
    if(expression.empty() || expression[0] != '(')  //variable
    {
      if(name[(name.size() - 1)] == '$')
      {
        result.type = str;
        result.stringValue =svb.get(name);
      }
      else
      {
        result.type = real;
        result.realValue = rvb.get(name);
      }
    }
    else       //function
    {
      vector<variable> arguments;
      int numberOfArguments = 0;
      string s;
      int parendepth = 1;
      expression.erase(0, 1);
      while(true)
      {
        if(expression.empty())
          errorAndTerminate("\')\' expected");
        if(expression[0] == '\"')
        {
          s.push_back('\"');
          expression.erase(0, 1);
          while(!expression.empty() && expression[0] != '\"')
          {
            s.push_back(expression[0]);
            expression.erase(0, 1);
          }
          if(expression.empty())
            errorAndTerminate("\'\"\' expected");
          s.push_back('\"');
          expression.erase(0, 1);
        }
        else if(expression[0] == '(')
        {
          s.push_back('(');
          expression.erase(0, 1);
          ++parendepth;
        }
        else if(expression[0] == ',')
        {
          if(parendepth > 1)
          {
            s.push_back(',');
            expression.erase(0, 1);
          }
          else
          {
            arguments.push_back(eval(s, rvb, svb));
            expression.erase(0, 1);
            s.clear();
          }
        }
        else if(expression[0] == ')')
        {
          --parendepth;
          if(parendepth == 0)
          {
            if(arguments.empty() && (LTRIM(s)).empty())
            {
              expression.erase(0, 1);
              break;
            }
            else
            {
              arguments.push_back(eval(s, rvb, svb));
              expression.erase(0, 1);
              break;
            }
          }
          s.push_back(')');
          expression.erase(0, 1);
        }
        else
        {
          s.push_back(expression[0]);
          expression.erase(0, 1);
        }
      }
      if(name == "ABS" && arguments.size() == 1 && arguments[0].type == real)
      {
        result.type = real;
        result.realValue = fabs(arguments[0].realValue);
      }
      else
      {
        cout << "Error: function " << name << "does not exist" << endl;
        exit(EXIT_SUCCESS);
      }
    }
  }
  else
  {
    cout << "Error: \'" << expression[0] << "\' parse error" << endl;
    exit(EXIT_SUCCESS);
  }
}

variable arith(char op, variable l, variable r)
{
  variable var;
  if(op == '+')
  {
    if(l.type == real && r.type == real)
    {
      var.type = real;
      var.realValue = l.realValue + r.realValue;
    }
    else if(l.type == str && r.type == str)
    {
      var.type = str;
      var.stringValue = l.stringValue + r.stringValue;
    }
    else
      errorAndTerminate("Type mismatch");
  }
  else if(op == '-')
  {
    if(l.type == real && r.type == real)
    {
      var.type = real;
      var.realValue = l.realValue - r.realValue;
    }
    else
      errorAndTerminate("Type mismatch");
  }
  else if(op == '*')
  {
    if(l.type == real && r.type == real)
    {
      var.type = real;
      var.realValue = l.realValue * r.realValue;
    }
    else
      errorAndTerminate("Type mismatch");
  }
  else
  {
    if(l.type == real && r.type == real)
    {
      if(r.realValue != 0)
      {
        var.type = real;
        var.realValue = l.realValue / r.realValue;
      }
      else
        errorAndTerminate("Division by zero");
    }
    else
      errorAndTerminate("Type mismatch");
  }
  return var;
}
